/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <journal/common//utils.hpp>
#include <journal/app/constants.h>
#include <journal/views/interfaces.h>
#include <journal/models/interfaces.h>
#include <journal/app/root.h>
#include <journal/app/constants.h>
#include <journal/app/keys.h>
#include <journal/models/photo_model_factory.h>
#include <journal/models/notebook_model_factory.h>
#include <journal/common/settings_service.hpp>
#include <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics/stats.hpp>
#include <boost/accumulators/statistics/rolling_mean.hpp>
#include <journal/common/util.h>

namespace journal { 

namespace m = baja::math;
namespace bt = baja::transition_tracking;

root::root(const std::shared_ptr<a::iinput_relay>& inputRelay) : 
    _inputRelay(inputRelay), 
    _inputObserverCookie(-1),
    _notebookObserverCookie(-1), 
    _isSuspending(false)
{}

std::shared_ptr<root> root::create(const std::shared_ptr<a::iinput_relay>& inputRelay)
{
    std::shared_ptr<root> returnValue = std::make_shared<root>(inputRelay);
    return returnValue;
}

void root::initialize()
{
    this->initView();

    // hook ourselves up to the input relay
    _inputObserverCookie = _inputRelay.lock()->addInputObserver(
        std::static_pointer_cast<a::iinput_observer>(shared_from_this()));
}

void root::onRender()
{
    _notebookView->onTick();
    _notebookView->onRender();
}

void root::onPointer(const a::pointer_args& args, void* context)
{
    bool handled = false;
    std::shared_ptr<a::iinput_observer> target = _capture.get();

// ifdef this out to disable fake mouse pressure
#if 1
    if (args.deviceType == a::device_type::mouse || args.deviceType == a::device_type::touch)
    {
        // fake the stylus pressure using the pointer velocity
        static b::float32 lastT = -1;
        static m::point2d lastPoint(-1, -1);
        static b::float32 vAverage = 0;
        static int samples = 0;
        typedef boost::accumulators::accumulator_set<b::float32, boost::accumulators::stats<boost::accumulators::tag::rolling_mean>> mean_acc;

        static const int sampleCount = 20;

        static mean_acc acc(boost::accumulators::tag::rolling_window::window_size = sampleCount);

        if (args.type == a::pointer_event_type::down)
        {
            acc = mean_acc(boost::accumulators::tag::rolling_window::window_size = sampleCount);
        }

        b::float32 t = static_cast<b::float32>(args.timestamp) / 1000000.f;

        // don't try this at home
        a::pointer_args* pa = const_cast<a::pointer_args*>(&args);

        if (lastT > 0)
        {
            m::vector2d diff;
            lastPoint.vectorTo(args.point, &diff);
            b::float32 lastPointDistance = diff.length();
            b::float32 timeDiff = t - lastT;

            if (timeDiff > 0 && lastPointDistance > 0)
            {
                b::float32 v = lastPointDistance / timeDiff; // pixels per second
                acc(v);
            }
        }

        b::float32 pressureDelta = 0.2f;
        if (boost::accumulators::rolling_count(acc) > 0)
        {
            pressureDelta = boost::accumulators::rolling_mean(acc) / 1000;
        }

        pa->pressure = max(0.2f, min(1, pressureDelta));

        lastT = t;
        lastPoint = pa->point;
    }
#endif

    this->updatePenDebugString(args);

    // if one of our children is capturing the input, pass it down directly to it
    if (target)
    {
        target->onPointer(args, static_cast<void*>(&handled));
    }
    else
    {
        _notebookView->onPointer(args, static_cast<void*>(&handled));
    }
}

void root::onResize(const a::resize_args& args)
{
    BAJA_TRACE(
        boost::format("onResize: (%i, %i)") %
        args.width %
        args.height
        );

    _notebookView->onResize(args);
}

void root::onScreenChange(const a::screen_change_args& args)
{
    _notebookView->onScreenChange(args);
}

void root::onGesture(const a::gesture_args& args, void* context)
{
    bool handled = false;
    std::shared_ptr<a::iinput_observer> target = _capture.get();

    // if one of our children is capturing the input, pass it down directly to it
    if (target)
    {
        target->onGesture(args, static_cast<void*>(&handled));
    }
    else
    {
        if (!handled)
        {
            _notebookView->onGesture(args, static_cast<void*>(&handled));
        }        
    }
}

void root::onVisibilityChanged(const bool visible)
{
    // use this chance to save the thumbnails
    if (!visible)
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
            L"SaveThumbnailsAsync: calling from onVisibilityChanged"
            );

        // update thumbnails
        _notebookView->saveThumbnailsAsync();

        // save the tile
        this->saveTileAsync([]() {});

        // save the page number
        DEBUG_CONSOLE_TRACE(boost::wformat(L"saving last page number: %i") % _notebookView->pageNumber());
		// TRACK: this cast is not safe (well.. it is, unless you have millions of pages really)
        constants::settings()->setInt((int)constants::general::id::lastPageNumber, static_cast<int>(_notebookView->pageNumber()));
    }
}

void root::onSuspend(const std::shared_ptr<a::isuspension_deferral>& deferral)
{
    if (_isSuspending) return;  // already suspending

    _deferral = deferral;

    // use this opportunity to save all the thumbnails
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
        L"SaveThumbnailsAsync: calling from onSuspend"
        );
     if (_notebookView->saveThumbnailsAsync())
     {
        _isSuspending = true;
     }
     else
     {
        _isSuspending = false; 
        // the thumbnails have been saved.  Shutdown the database.
        if (constants::general::shutdownDBSessionOnSuspend() == true)
        {
            constants::storageService()->engine()->shutdown();
        }
        _deferral->complete();
        DEBUG_CONSOLE_TRACE(L"suspension completed");
     }	
}

void root::saveTileAsync(std::function<void()> f)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS, L"root::saveTileAsync -  saving tile image");
    _notebookView->generateTileImageAsync(
        [f](const bool isEmpty, const std::wstring& fileName)
        { 
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS, L"root::saveTileAsync - tile image saved");
            if (isEmpty)
            {
                constants::shellService()->clearTile();
            }
            else
            {
                constants::shellService()->updateTile(fileName);
            }
            f();
        });
}

void root::onThumbnailSaved(const std::shared_ptr<view::ipage>& page)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS, 
        boost::wformat(L"root::onThumbnailSaved - saved thumbnail for page %i ") % page->model()->pageNumber());
    
    if (!_isSuspending) return;

    if (_notebookView->areThumbnailsSaved())
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS, L"root::onThumbnailSaved - all thumbnail saves completed.  completing suspension. ");
        // the thumbnails have been saved.  Shutdown the database.
        if (constants::general::shutdownDBSessionOnSuspend() == true)
        {
            constants::storageService()->engine()->shutdown();
        }
        _deferral->complete();
        DEBUG_CONSOLE_TRACE(L"suspension completed");
    }
}

void root::onKey(const a::key_args& args, void* context)
{
    bool handled = false;
    std::shared_ptr<a::iinput_observer> target = _capture.get();

    // if one of our children is capturing the input, pass it down directly to it
    if (target)
    {
        target->onKey(args, static_cast<void*>(&handled));
    }
    else
    {
        if (journal::app::key_registry::check(journal::app::key::runTests, args.vk))
        {
            constants::automationService()->runEnabledTestsAsync();
        }
        else
        {
            _notebookView->onKey(args, static_cast<void*>(&handled));
        }
    }
}

void root::onStrokeAdded(const std::shared_ptr<model::iink_stroke_model>& newStroke)
{
    // add the stroke to the model
    newStroke->parent()->attachInkStroke(newStroke);
}

void root::onPhotoAdded(const std::shared_ptr<view::iphoto>& photo)
{
    // observe the photo
    photo->addObserver(std::static_pointer_cast<iphoto_observer>(shared_from_this()));
}

void root::onPhotoMoved(
    const std::shared_ptr<view::iphoto>& photo,
    const b::float32& newScale,
    const m::point2d& newPosition,
    const b::float32& newAngle
    )
{
    // update the photo model
    photo->model()->setSpatialAttributes(newScale, newPosition, newAngle);
}

void root::onPhotoSelected(
    const std::shared_ptr<view::iphoto>& photo
    )
{
}

void root::onPhotoDeselected()
{
}

void root::onPhotoRemoved(const std::shared_ptr<view::iphoto>& photo)
{
    // remove the photo from the model
    photo->model()->parent()->removePhoto(photo->model()->storageId());
    
    // stop observing the photo view
    photo->removeObserver(std::static_pointer_cast<iphoto_observer>(shared_from_this()));
}

void root::initView()
{
    // create a new notebook model
    _notebookModel = model::createNotebookModel();

    // hook the undo-redo service
    std::shared_ptr<bt::itransition_target> undoRedoRoot = std::dynamic_pointer_cast<baja::transition_tracking::itransition_target>(_notebookModel);
    journal::constants::undoRedoService()->registerRootTransitionTarget(undoRedoRoot);

    // create the notebook view
    _notebookView = view::createNotebook(
        _notebookModel, 
        std::static_pointer_cast<a::iinput_capture>(shared_from_this()));

    std::shared_ptr<root> p_this = shared_from_this();

    // hook up to the views observer interfaces
    p_this->_notebookObserverCookie = p_this->_notebookView->addObserver(std::static_pointer_cast<view::inotebook_observer>(p_this));

    _penDebugString.resize(max_debug_string);
    _penDebugString.push_back(L'');
    constants::debugConsole()->getRoot()->addStringVar(
        L"pen",
        _penDebugString.data(),
        0
        );
}

void root::cleanupView()
{
    if (_notebookView)
    {
        _notebookView->removeObserver(_notebookObserverCookie);
        _notebookObserverCookie = -1;
    }

    _notebookView = nullptr;
    _notebookModel = nullptr;
}

void root::cleanup()
{
    if (_inputRelay.lock()) _inputRelay.lock()->removeInputObserver(_inputObserverCookie);
    _inputObserverCookie = -1;
    _settingsObserverCookie = -1;
    this->cleanupView();
}

void root::updatePenDebugString(const a::pointer_args& args)
{
    if (!constants::debugConsole()->getIsVisible())
    {
        return;
    }

    std::wstring touchIds;
    b::uint32 touchCount = constants::shellService()->touchPointCount();
    
    std::wostringstream outs;
    if (touchCount == 0)
    {
        outs << L"--";
    }
    else
    {
        bool first = true;
        std::for_each(
            constants::shellService()->touchPoints().begin(),
            constants::shellService()->touchPoints().end(),
            [&first, &outs](const std::pair<int, a::pointer_args>& p)
        {
            if (!first) outs << ", ";
            outs << p.second.id;
            first = false;
        });
    }

    boost::wformat fmt = 
        boost::wformat(L"pos: (%0.2f, %0.2f), prs: %0.2f, touch: %i (%s)") %
            args.point.x %
            args.point.y %
            args.pressure %
            constants::shellService()->touchPointCount() %
            outs.str();

    std::wstring s = fmt.str();

    std::copy(s.begin(), s.end(), _penDebugString.begin());
    _penDebugString[min(s.size(), max_debug_string)] = L'\0';
}

void root::onShareImageAsync(view::copy_info& info, std::function<void(const std::shared_ptr<view::ishared_image>&)> f)
{
    _notebookView->onShareImageAsync(info, f);
}

void root::onCopyImageAsync(view::copy_info& info, std::function<void(const std::shared_ptr<view::ishared_image>&)> f)
{
    _notebookView->onCopyImageAsync(info, f);
}

void root::onPasteImageAsync(const std::shared_ptr<view::ishared_image>& image, std::function<void()> f)
{
    _notebookView->onPasteImageAsync(image, f);
}

void root::onCommand(const view::command_id id, void* context)
{
    bool* handled = static_cast<bool*>(context);

    if (id == view::command_id::pen_1 || 
        id == view::command_id::pen_2 || 
        id == view::command_id::pen_3 || 
        id == view::command_id::pen_4 ||
        id == view::command_id::pen_5
        )
    {
        constants::settings()->setFloat((int)constants::ink::id::baseWidth, common::commandIdToWidth(id));
        *handled = true;
    }
    else if (id == view::command_id::color_black || 
        id == view::command_id::color_blue || 
        id == view::command_id::color_green || 
        id == view::command_id::color_pink ||
        id == view::command_id::color_red ||
        id == view::command_id::color_yellow
        )
    {
        constants::settings()->setEnum((int)constants::ink::id::inkColor, common::commandIdToColor(id));
        *handled = true;
    }
    else if (id == view::command_id::tool_eraser || id == view::command_id::tool_pen)
    {
        constants::settings()->setEnum((int)constants::ink::id::inkTool, common::commandIdToInkTool(id));
        // do not mark as handled since we want to notify other elements of the command.
    }

    if (!*handled)
    {
        _notebookView->onCommand(id, context);
    }
}

void root::onPhotoFileSelected(const std::shared_ptr<b::storage::istorage_file>& file)
{
    _notebookView->onPhotoFileSelected(file);
}

} // namespace journal
